/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2022. All rights reserved.
 *
 */

import Buffer from 'buffer';
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';

import { OhosLogger } from './log/ohos-logger.js';

/**
 * <p>密钥解码器</p>
 * <p>主要方法是#getKey()，用于读取工作密钥。</p>
 * <p>工作密钥采用AES-GCM方式进行解密，填充参数是NoPadding，密钥长度16 byte(128 bit)，iv长度12 byte(96bit)，iv每次都是采用SecureRandom生成，
 * tag参数长度128 bit。</p>
 * <p>根密钥由存储在文件中的组件以及硬编码在代码中的组件做异或操作得到一个组件，然后将该组件使用PBKDF2WithHmacSHA256迭代10000次生成</p>
 * <p>根据密钥管理规范，存储组件的文件名以及目录已采用一般化名称存储，未体现其是密钥等信息</p>
 * <p>主要参考文档《密钥管理安全规范 V1.3.docx》、《密码算法应用规范 V1.4.docx》、《对称加密算法应用方案.docx》</p>
 *
 * @since 2022/03/07
 */
export class DecipherUtil {
  private static component = new Int8Array([0x31, 0xf3, 0x09, 0x73, 0xd6, 0xaf, 0x5b, 0xb8, 0xd3, 0xbe, 0xb1, 0x58, 0x65, 0x83, 0xc0, 0x77]);
  private static dirs: string[] = ['fd', 'ac', 'ce'];

  private static _logger = OhosLogger.getLogger('Decipher');

  private static macDSStore: string = '.DS_Store';

  // 存储给报错信息用的
  private static materialDir: string;

  /**
   * 使用aes-gcm算法解密消息
   * AesGcmKit，其实现符合《密码算法应用规范 V1.4.docx》中针对于AES-GCM的使用要求
   * 填充参数是NoPadding，密钥长度16 byte(128 bit)，iv长度12 byte(96bit)，iv每次都是采用SecureRandom生成，tag参数长度16 byte(128 bit)。
   *
   * @param materialDir 签名材料地址
   * @param encryptedPwd 加密密码
   * @return string
   */
  static decryptPwd(materialDir: string, encryptedPwd: string): string {
    this.materialDir = materialDir;
    if (32 > encryptedPwd.length) {
      this._logger.printErrorExit('INVALID_DATA', [this.getProjectBuildProfilePath()], [[this.getProjectBuildProfilePath()]]);
    }
    if (0 !== encryptedPwd.length % 2) {
      this._logger.printErrorExit('INVALID_PASSWORD_LENGTH', [this.getProjectBuildProfilePath()], [[this.getProjectBuildProfilePath()]]);
    }
    let result: Buffer.Buffer = Buffer.Buffer.from('');
    const key = DecipherUtil.getKey(materialDir);
    const msgBytes = new Int8Array(Buffer.Buffer.from(encryptedPwd, 'hex'));
    result = DecipherUtil.decrypt(key, msgBytes);
    return result.toString('utf-8');
  }

  /**
   * 获取解密密钥
   *
   * @param materialDir 签名材料地址
   * @private Int8Array
   */
  private static getKey(materialDir: string): Int8Array {
    const material = path.resolve(materialDir, 'material');
    if (!fs.statSync(material).isDirectory()) {
      this._logger.printErrorExit('SIGNING_FAILED_NO_SIGNING_NATERIALS_IS_NOT_A_DIRECTORY',
        [material, this.getProjectBuildProfilePath()],
        [[material], [this.getProjectBuildProfilePath()], [materialDir]]);
    }
    // 解密时过滤.DS_Store文件
    const files = fs.readdirSync(material).filter((file) => file != this.macDSStore);
    if (0 === files.length) {
      this._logger.printErrorExit('SIGNING_FAILED_NO_SIGNING_NATERIALS_IS_AN_EMPTY_DIRECTORY',
        [material, this.getProjectBuildProfilePath()],
        [[material], [this.getProjectBuildProfilePath()], [materialDir]]);
    }
    this.dirs.forEach((dirName) => {
      if (!files.includes(dirName)) {
        this._logger.printErrorExit('SIGNING_FAILED_CAN_NOT_FIND_SIGNING_MATERIAL',
          [dirName, this.getProjectBuildProfilePath()],
          [[this.dirs.join('、'), material], [this.getProjectBuildProfilePath()], [materialDir]]);
      }
    });
    const components = this.readFd(material, this.dirs[0]);
    const salt = this.readSalt(path.resolve(material, this.dirs[1]));
    const rootKey = this.getRootKey(components, salt);
    const workMaterial = this.readWorkMaterial(path.resolve(material, this.dirs[2]));

    return new Int8Array(DecipherUtil.decrypt(rootKey, workMaterial));
  }

  /**
   * PBKDF2WithHmacSHA256迭代10000次生成rootKey
   *
   * @param components
   * @param salt
   * @private
   */
  private static getRootKey(components: Int8Array[], salt: Int8Array) {
    const full = components.concat(this.component);
    const finalComponents = this.xorComponents(full);
    const buffer = crypto.pbkdf2Sync(finalComponents.toString(), salt, 10000, 16, 'sha256');
    return new Int8Array(buffer);
  }

  /**
   * 根据密钥解密, 获取明文密码
   * 被加密后的数据格式如下
   * 其中content数据中包含tag数据
   * -------------------------------------------------------------------------------
   * | content length (4byte) | iv key (12 byte) | encrypted | auth tag ( 16 byte ) |
   *  ------------------------------------------------------------------------------
   *
   * @param key 密钥
   * @param msg 加密密码
   * @private Buffer.Buffer
   */
  private static decrypt(key: Int8Array, msg: Int8Array) {
    try {
      const len = ((msg[0] & 0xff) << 24) | ((msg[1] & 0xff) << 16) | ((msg[2] & 0xff) << 8) | (msg[3] & 0xff);
      const ivLen = msg.length - 4 - len;
      const ivKey = msg.slice(4, 4 + ivLen);
      const decipherGCM = crypto.createDecipheriv('aes-128-gcm', key, ivKey);
      const authTag = msg.slice(msg.length - 16);
      decipherGCM.setAuthTag(authTag);

      const update = decipherGCM.update(msg.subarray(4 + ivLen, msg.length - 16));
      const final = decipherGCM.final();
      return Buffer.Buffer.concat([update, final]);
    } catch(e: any) {
      this._logger.printErrorExit('KEY_PASSWORD_VALID_FAILED', [e.message, this.getProjectBuildProfilePath()], [[]]);
    }
    return Buffer.Buffer.alloc(0);
  }

  private static xorComponents(components: Int8Array[]) {
    components.forEach((value) => {
      if (16 !== value.length) {
        this._logger.printErrorExit('SIGNING_FAILED_SIGZNING_MATERIAL_DATA_ERROR',
          [this.getProjectBuildProfilePath()], [[this.getProjectBuildProfilePath()], [this.materialDir]]);
      }
    });
    let result = this.xor(components[0], components[1]);
    for (let i = 2; i < components.length; i++) {
      result = this.xor(result, components[i]);
    }
    return Buffer.Buffer.from(result);
  }

  /**
   * 异或
   *
   * @param b1
   * @param b2
   * @private
   */
  private static xor(b1: Int8Array, b2: Int8Array): Int8Array {
    if (b1.byteLength !== b2.byteLength) {
      this._logger.printErrorExit('SIGNING_FAILED_SIGZNING_MATERIAL_DATA_ERROR',
        [this.getProjectBuildProfilePath()], [[this.getProjectBuildProfilePath()], [this.materialDir]]);
    }
    const result: Int8Array = new Int8Array(b1.byteLength);
    for (let i = 0; i < b1.byteLength; i++) {
      result[i] = b1[i] ^ b2[i];
    }
    return result;
  }

  /**
   * 读取./material/fd
   *
   * @param dir
   * @private
   */
  private static readFd(material: string, fd: string) {
    const dir = path.resolve(material, fd);
    if (!fs.existsSync(dir)) {
      this._logger.printErrorExit('SIGNING_FAILED_CAN_NOT_FIND_SIGNING_MATERIAL',
        [fd, this.getProjectBuildProfilePath()],
        [[this.dirs.join('、'), material], [this.getProjectBuildProfilePath()], [this.materialDir]]);
    }
    const names = fs.readdirSync(dir).filter((file) => file != this.macDSStore);
    if (3 !== names.length) {
      this._logger.printErrorExit('SIGNING_FAILED_SIGNING_MATERIAL_IS_ILLEGAL',
        [dir, this.getProjectBuildProfilePath()],
        [[dir], [this.getProjectBuildProfilePath()], [this.materialDir]]);
    }
    const result: Int8Array[] = [];
    names.forEach((value) => result.push(this.readDirBytes(path.resolve(dir, value))));
    return result;
  }

  /**
   * 读取盐值./material/ac
   *
   * @param dir
   * @private
   */
  private static readSalt(dir: string): Int8Array {
    return this.readDirBytes(dir);
  }

  /**
   * workMaterial ./material/ce
   *
   * @param dir
   * @private
   */
  private static readWorkMaterial(dir: string) {
    return this.readDirBytes(dir);
  }

  /**
   * 读取目录中文件 默认一个文件
   *
   * @param dir
   * @private
   */
  private static readDirBytes = (dir: string): Int8Array => {
    if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
      DecipherUtil._logger.printErrorExit('SIGNING_FAILED_CAN_NOT_FIND_THE_SIGZNING_MATERIAL',
        [this.getProjectBuildProfilePath()], [[dir], [this.getProjectBuildProfilePath()], [this.materialDir]]);
    }
    const names = fs.readdirSync(dir);
    if (1 !== names.length) {
      DecipherUtil._logger.printErrorExit('SIGNING_FAILED_SIGZNING_MATERIAL_ERROR',
        [this.getProjectBuildProfilePath()], [[dir], [this.getProjectBuildProfilePath()], [this.materialDir]]);
    }
    const buffer = fs.readFileSync(path.resolve(dir, names[0]));
    return new Int8Array(buffer);
  };

  /**
   * 读取默认的密钥库密码
   *
   * @param DEFAULT_STORE_PASS_FILE 存储加密后的密钥库密码的文件路径
   * @return string 文件中存储的加密的密码信息
   */
  static decryptPluginDataFromResource(DEFAULT_STORE_PASS_FILE: string): string {
    const filePath = path.resolve(__dirname, DEFAULT_STORE_PASS_FILE);
    const content = fs.readFileSync(filePath).toString();
    return DecipherUtil.decryptPwd(path.resolve(__dirname, '../../res'), content);
  }

  private static getProjectBuildProfilePath() {
    return path.resolve(process.cwd(), 'build-profile.json5');
  }
}
